package com.bolt.support.spring.mvc.advice;

import com.bolt.common.utils.StrUtil;
import com.bolt.common.utils.ValidationUtil;
import com.bolt.convention.data.ResultMessage;
import com.bolt.convention.data.Results;
import com.bolt.convention.data.code.CommonCode;
import com.bolt.convention.exception.ServiceException;
import com.bolt.convention.exception.ServiceValidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.InvalidPropertyException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 *全局捕捉异常
 */
@RestControllerAdvice
public class GlobalRestControllerExceptionHandler {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired(required = false)
    private GlobalRestControllerCustomExceptionHandler customExceptionHandler;

    @ExceptionHandler(value = {NoHandlerFoundException.class})
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResultMessage noHandlerFoundException(HttpServletRequest request,
                                                 NoHandlerFoundException ex) {

        logThrowable(false, request, ex);
        return Results.failure(CommonCode.DATA_NOT_FOUND);
    }

    @ExceptionHandler(value = {ConstraintViolationException.class})
    public ResultMessage constraintViolationException(HttpServletRequest request,
                                               ConstraintViolationException ex) {

        logThrowable(false, request, ex);

        List<ResultMessage.ViolationItem> violationItems = ValidationUtil.convertToResultViolationItems(
                ex.getConstraintViolations());

        if (violationItems != null) {
            return Results.invalid(violationItems);
        }
        return Results.invalid();
    }

    @ExceptionHandler(value = {MethodArgumentNotValidException.class})
    public ResultMessage methodArgumentNotValidException(HttpServletRequest request,
                                                  MethodArgumentNotValidException ex) {
        List<FieldError> errorList = ex.getBindingResult().getFieldErrors();
        ResultMessage result = invalid(errorList);
        if (logger.isInfoEnabled()) {
            logger.info("[" + request.getMethod() + "] " + request.getRequestURI() +
                    (StringUtils.isEmpty(request.getQueryString()) ? "" : "?" + request.getQueryString()) + " " +
                    ex.getClass().getName() + ":" + result.toString());
        }
        return result;
    }

    private ResultMessage invalid(List<FieldError> errors) {
        if (errors != null && errors.size() > 0) {
            return Results.invalid(errors.stream().map(x ->
                    Results.buildViolationItem(x.getField(),
                            x.getDefaultMessage())
            ).collect(Collectors.toList()));
        }
        return Results.invalid();
    }

    @ExceptionHandler(value = {BindException.class})
    public ResultMessage bindException(HttpServletRequest request,
                                BindException e) {

        List<FieldError> errorList = e.getBindingResult().getFieldErrors();
        ResultMessage result = invalid(errorList);

        if (logger.isInfoEnabled()) {
            logger.info("[" + request.getMethod() + "] " + request.getRequestURI() +
                    (StringUtils.isEmpty(request.getQueryString()) ? "" : "?" + request.getQueryString()) + " " +
                    e.getClass().getName() + ":" + result.toString());
        }
        return result;
    }

    @ExceptionHandler(value = {MissingServletRequestParameterException.class})
    public ResultMessage missingServletRequestParameterException(
            HttpServletRequest request,
            MissingServletRequestParameterException e) {

        logThrowable(false, request, e);
        return Results.invalid(CommonCode.INVALID_ARGS.message()).addViolationItem(e.getParameterName(),
                "不能为空");
    }

    @ExceptionHandler(value = {InvalidPropertyException.class})
    public ResultMessage invalidPropertyException(HttpServletRequest request,
                                           InvalidPropertyException e) {
        logThrowable(false, request, e);
        return Results.invalid().addViolationItem(e.getPropertyName(), CommonCode.INVALID_ARGS.message());
    }

    @ExceptionHandler(value = {HttpRequestMethodNotSupportedException.class})
    public ResultMessage httpRequestMethodNotSupportedException(
            HttpServletRequest request,
            HttpRequestMethodNotSupportedException ex) {

        logThrowable(false, request, ex);
        return Results.failure(CommonCode.METHOD_NOT_ALLOWED);
    }

    @ExceptionHandler(value = {ServletRequestBindingException.class})
    public ResultMessage servletRequestBindingException(HttpServletRequest request,
                                                 ServletRequestBindingException ex) {

        logThrowable(false, request, ex);
        return Results.invalid(ex.getMessage());
    }

    @ExceptionHandler(value = {ServiceValidException.class})
    public ResultMessage serviceException(HttpServletRequest request,
                                   ServiceValidException ex) {
        logThrowable(false, request, ex);
        return Results.invalid(ex.getMessage(), ex.getViolationItems());
    }




    @ExceptionHandler(value = {ServiceException.class})
    public ResultMessage serviceException(HttpServletRequest request,
                                   ServiceException ex) {
        logThrowable(false, request, ex);
        return Results.failure(ex);
    }



    @ExceptionHandler(value = Throwable.class)
    public ResultMessage defaultErrorHandler(HttpServletRequest request,
                                      Throwable throwable) {
        // 先行执行自定义异常增强
        Optional<ResultMessage> handleResult = this.customExceptionHandling(throwable);
        if (handleResult.isPresent()) {
            return handleResult.get();
        }
        logThrowable(true, request, throwable);
        return Results.error(throwable);
    }

    private Optional<ResultMessage> customExceptionHandling(Throwable e) {
        if (Objects.isNull(customExceptionHandler)) {
            return Optional.empty();
        }
        ResultMessage result = null;
        try {
            result = customExceptionHandler.handleGlobalRestControllerException(e);
        } catch (Throwable e1) {
            logger.error("Return result exception advice occurrence of user exception", e1);
        }
        return Optional.ofNullable(result);
    }

    private void logThrowable(boolean error, HttpServletRequest request, Throwable throwable) {
        if (error) {
            logger.error("[" + request.getMethod() + "] " + request.getRequestURI() +
                            (StrUtil.isEmpty(request.getQueryString()) ? "" : "?" + request.getQueryString()) + " ",
                    throwable);
        } else if (logger.isInfoEnabled()) {
            logger.info("[" + request.getMethod() + "] " + request.getRequestURI() +
                    (StrUtil.isEmpty(request.getQueryString()) ? "" : "?" + request.getQueryString()) + " " +
                    (throwable instanceof ServiceException ?
                            throwable.toString() :
                            (throwable.getClass().getName() + ":" + throwable.getMessage())));
        }
    }
}
